昨天我們看完了文章總覽頁
今天我們進到文章內頁
程式碼如下:
import { Icon } from '@chakra-ui/icons'
// ~中間略~
const messages = defineMessages({
prevPost: { id: 'blog.common.prevPost', defaultMessage: '上一則' },
nextPost: { id: 'blog.common.nextPost', defaultMessage: '下一則' },
blogSuggestion: { id: 'blog.label.blogSuggestion', defaultMessage: '回應' },
})
const StyledTitle = styled.div`
// ~中間略~
`
const StyledTag = styled.span`
// ~中間略~
`
const StyledLabel = styled.div`
// ~中間略~
`
const StyledSubTitle = styled.div`
// ~中間略~
`
const StyledPostTitle = styled.h3`
// ~中間略~
`
const BlogPostPage: React.VFC = () => {
const { formatMessage } = useIntl()
const { currentMemberId } = useAuth()
const { searchId } = useParams<{ searchId: string }>()
const app = useApp()
const { loadingPost, post, refetchPosts } = usePost(searchId)
const postId = post?.id
const addPostView = useAddPostViews()
const { insertPostReaction, deletePostReaction } = useMutatePostReaction(postId)
const [isScrollingDown, setIsScrollingDown] = useState(false)
const [isLiked, setIsLiked] = useState(false)
const handleGetPostLikes = () => {
const postLikesData: { postId: string }[] = JSON.parse(localStorage.getItem('kolabe.post_reaction') || '[]')
const isThisPostLikes: boolean = postLikesData.some(v => v.postId === postId)
setIsLiked(isThisPostLikes)
}
useEffect(() => {
document.getElementById('layout-content')?.scrollTo({ top: 0 })
handleGetPostLikes()
}, [postId])
const handleScroll = useCallback(
throttle(() => {
const postCoverElem = document.querySelector('#post-cover')
const layoutContentElem = document.querySelector('#layout-content')
if (!postCoverElem || !layoutContentElem) {
return
}
if (layoutContentElem.scrollTop > postCoverElem.scrollHeight) {
if (isScrollingDown) {
return
}
setIsScrollingDown(true)
} else {
setIsScrollingDown(false)
}
}, 100),
[post],
)
useEffect(() => {
const layoutContentElem = document.querySelector('#layout-content')
if (!layoutContentElem) {
return
}
layoutContentElem.addEventListener('scroll', () => handleScroll())
return layoutContentElem.removeEventListener('scroll', () => handleScroll())
}, [handleScroll])
if (!app.loading && !app.enabledModules.blog) {
return <ForbiddenPage />
}
if (loadingPost) {
return <LoadingPage />
}
if (!post) {
return <NotFoundPage />
}
try {
const visitedPosts = JSON.parse(sessionStorage.getItem('kolable.posts.visited') || '[]') as string[]
if (!visitedPosts.includes(post.id)) {
visitedPosts.push(post.id)
sessionStorage.setItem('kolable.posts.visited', JSON.stringify(visitedPosts))
addPostView(post.id)
}
} catch (error) {}
const handleLikeStatus = async () => {
if (isLiked) {
await deletePostReaction()
setIsLiked(false)
} else {
await insertPostReaction()
setIsLiked(true)
}
await refetchPosts()
}
return (
<DefaultLayout white noHeader={isScrollingDown}>
<BlogPostPageHelmet post={post} />
<div className="container py-sm-5">
<div className="row justify-content-center">
<div className="col-12 col-lg-9">
{!loadingPost && (
<PostCover
title={post?.title || ''}
coverUrl={post?.videoUrl || post?.coverUrl || null}
type={post?.videoUrl ? 'video' : 'picture'}
merchandises={post?.merchandises || []}
isScrollingDown={isScrollingDown}
/>
)}
<StyledPostMeta className="pb-3">
<Icon as={UserOIcon} className="mr-1" />
<span className="mr-2">{post?.author.name}</span>
<Icon as={CalendarAltOIcon} className="mr-1" />
<span className="mr-2">{post?.publishedAt ? moment(post.publishedAt).format('YYYY-MM-DD') : ''}</span>
<Icon as={EyeIcon} className="mr-1" />
<span>{post?.views}</span>
</StyledPostMeta>
<StyledTitle>{post?.title}</StyledTitle>
<StyledPostMeta className="pb-3">{post.source}</StyledPostMeta>
<div className="mb-5">
{loadingPost ? (
<SkeletonText mt="1" noOfLines={4} spacing="4" />
) : (
<BraftContent>{post?.description}</BraftContent>
)}
</div>
<div className="row mb-5">
<div className="col-6 col-lg-4">
{post?.tags.map(tag => (
<Link key={tag} to={`/posts/?tags=${tag}`} className="mr-2">
<StyledTag>#{tag}</StyledTag>
</Link>
))}
</div>
<div className="col-6 col-lg-4 offset-lg-4 d-flex align-items-center justify-content-end">
<SocialSharePopover url={window.location.href} />
<LikesCountButton onClick={handleLikeStatus} count={post.reactedMemberIdsCount} isLiked={isLiked} />
</div>
</div>
<Divider className="mb-3" />
<div className="py-3">
{post?.author && (
<CreatorCard
id={post.author.id}
avatarUrl={post.author.avatarUrl}
title={post.author.name}
labels={[]}
description={post.author.abstract || ''}
withProgram
withPodcast
withAppointment
withBlog
noPadding
/>
)}
</div>
<Divider className="mb-5" />
<div className="row mb-5">
<div className="col-6 col-lg-4">
{post?.prevPost && (
<Link to={`/posts/${post.prevPost.codeName || post.prevPost.id}`}>
<StyledLabel>{formatMessage(messages.prevPost)}</StyledLabel>
<StyledSubTitle>{post.prevPost.title}</StyledSubTitle>
</Link>
)}
</div>
<div className="col-6 col-lg-4 offset-lg-4">
{post?.nextPost && (
<Link to={`/posts/${post.nextPost.codeName || post.nextPost.id}`} className="text-right">
<StyledLabel>{formatMessage(messages.nextPost)}</StyledLabel>
<StyledSubTitle>{post.nextPost.title}</StyledSubTitle>
</Link>
)}
</div>
</div>
<div className="row">{postId && <RelativePostCollection postId={postId} tags={post?.tags} />}</div>
<div className="mb-4">
<StyledPostTitle className="mb-3">{formatMessage(messages.blogSuggestion)}</StyledPostTitle>
{currentMemberId && (
<SuggestionCreationModal threadId={`/posts/${postId}`} onRefetch={() => refetchPosts()} />
)}
{post?.suggests.map(v => (
<div key={v.id}>
<MessageSuggestItem
key={v.id}
suggestId={v.id}
memberId={v.memberId}
description={v.description}
suggestReplyCount={v.suggestReplyCount}
programRoles={post?.postRoles || []}
reactedMemberIds={v.reactedMemberIds}
createdAt={v.createdAt}
onRefetch={() => refetchPosts()}
/>
</div>
))}
</div>
</div>
</div>
</div>
</DefaultLayout>
)
}
export default BlogPostPage
裡面也分成了很多 Component
在元件最上方有一個「BlogPostPageHelmet」
是專門 for SEO 和 OpeGraph 的設定
接著就是文章本體
首先最上方是文章的 Banner
下芳則是有關文章的 Meta Data
再來就是文章本身
它是由「BraftContent」這個 Component
解析從後台儲存的編輯器文字,轉成 HTML 的格式呈現
文章的最下方顯示這個文章有的標籤
點擊後會找尋這個標籤相關的文章
使用 react-router-dom 的 Link 完成
旁邊有分享和按讚的按鈕,分別使用「SocialSharePopover」和「LikesCountButton」
再來是發文者的資訊
他是使用「CreatorCard」這個元件完成
最後則是文章相關的區塊
分別是看上一篇和下一篇文章
以及相關的文章,由「RelativePostCollection」完成
帶入指定的 Tag,就會返回相關的文章
最後則是回覆的功能,這塊涵蓋了許多元件,就省略不講
明天我們來看探索課程的部分